/*!
 * Copyright (c) Microsoft Corporation and contributors. All rights reserved.
 * Licensed under the MIT License.
 */

import type {
	FluidObject,
	IDisposable,
	ITelemetryBaseLogger,
} from "@fluidframework/core-interfaces";
import type {
	IClientDetails,
	IQuorumClients,
	ISummaryTree,
} from "@fluidframework/driver-definitions";
import type {
	ISnapshot,
	IDocumentMessage,
	ISnapshotTree,
	ISummaryContent,
	IVersion,
	MessageType,
	ISequencedDocumentMessage,
	ICreateBlobResponse,
	ISummaryContext,
	ISnapshotFetchOptions,
	FetchSource,
	IDocumentStorageServicePolicies,
} from "@fluidframework/driver-definitions/internal";

import type { IAudience } from "./audience.js";
import type { IDeltaManager } from "./deltas.js";
import type { ICriticalContainerError } from "./error.js";
import type { ConnectionState, ILoader } from "./loader.js";

/**
 * The attachment state of some Fluid data (e.g. a container or data store), denoting whether it is uploaded to the
 * service.  The transition from detached to attached state is a one-way transition.
 * @public
 */
export enum AttachState {
	/**
	 * In detached state, the data is only present on the local client's machine.  It has not yet been uploaded
	 * to the service.
	 */
	Detached = "Detached",

	/**
	 * In attaching state, the data has started the upload to the service, but has not yet completed.
	 */
	Attaching = "Attaching",

	/**
	 * In attached state, the data has completed upload to the service.  It can be accessed by other clients after
	 * reaching attached state.
	 */
	Attached = "Attached",
}

/**
 * Defines the connection status of the container.
 *
 * @legacy @beta
 * @sealed
 */
export interface ConnectionStatusTemplate {
	connectionState: ConnectionState;
	/**
	 * True if the runtime is currently allowed to send ops.
	 */
	canSendOps: boolean;
	/**
	 * True if the container is in readonly mode.
	 */
	readonly: boolean;
}

/**
 * Status for an establishing connection.
 *
 * @legacy @beta
 * @sealed
 */
export interface ConnectionStatusEstablishingConnection extends ConnectionStatusTemplate {
	connectionState: ConnectionState.EstablishingConnection;
	canSendOps: false;
}

/**
 * Status for an in-progress connection that is catching up to latest op state.
 *
 * @legacy @beta
 * @sealed
 */
export interface ConnectionStatusCatchingUp extends ConnectionStatusTemplate {
	connectionState: ConnectionState.CatchingUp;
	/**
	 * The ID of the client connection that is connecting.
	 */
	pendingClientConnectionId: string;
	canSendOps: false;
}

/**
 * Status for a full connection.
 *
 * @legacy @beta
 * @sealed
 */
export interface ConnectionStatusConnected extends ConnectionStatusTemplate {
	connectionState: ConnectionState.Connected;
	/**
	 * The ID of the client connection that is connected.
	 */
	clientConnectionId: string;
	canSendOps: boolean;
}

/**
 * Status for a disconnected connection.
 *
 * @legacy @beta
 * @sealed
 */
export interface ConnectionStatusDisconnected extends ConnectionStatusTemplate {
	connectionState: ConnectionState.Disconnected;
	canSendOps: false;
	/**
	 * The ID of the client connection that was most recently connecting (catching up).
	 */
	priorPendingClientConnectionId: string | undefined;
	/**
	 * The ID of the client connection that was most recently connected.
	 */
	priorConnectedClientConnectionId: string | undefined;
}

/**
 * Connection status of the container.
 *
 * @legacy @beta
 * @sealed
 */
export type ConnectionStatus =
	| ConnectionStatusEstablishingConnection
	| ConnectionStatusCatchingUp
	| ConnectionStatusConnected
	| ConnectionStatusDisconnected;

/**
 * The IRuntime represents an instantiation of a code package within a Container.
 * Primarily held by the ContainerContext to be able to interact with the running instance of the Container.
 *
 * @legacy @beta
 */
export interface IRuntime extends IDisposable {
	/**
	 * Notifies the runtime of a change in the connection state
	 * @param canSendOps - true if the runtime is allowed to send ops
	 * @param clientId - the ID of the client that is connecting or disconnecting
	 *
	 * @remarks
	 * This is deprecated when used with `@fluidframework/container-loader` v2.63
	 * and later. Implement {@link IRuntime.setConnectionStatus}. Then this method
	 * will not be called, when using newer container-loader.
	 *
	 * Note: when {@link IRuntime.setConnectionStatus} is implemented, there will
	 * not be a call exactly when Container loads as happens currently without it.
	 */
	setConnectionState(canSendOps: boolean, clientId?: string);

	/**
	 * Notifies the runtime of a change in the connection state.
	 *
	 * @remarks This supersedes {@link IRuntime.setConnectionState}.
	 */
	setConnectionStatus?(status: ConnectionStatus): void;

	/**
	 * Processes the given op (message)
	 * @param message - delta message received from the server
	 * @param local - true if the message was originally generated by the client receiving it.
	 */
	process(message: ISequencedDocumentMessage, local: boolean);

	/**
	 * Processes the given signal
	 */
	// TODO: use `unknown` instead (API breaking)
	// eslint-disable-next-line @typescript-eslint/no-explicit-any
	processSignal(message: any, local: boolean);

	/**
	 * Create a summary. Used when attaching or serializing a detached container.
	 *
	 * @param blobRedirectTable - A table passed during the attach process. While detached, blob upload is supported
	 * using IDs generated locally. After attach, these IDs cannot be used, so this table maps the old local IDs to the
	 * new storage IDs so requests can be redirected.
	 */
	createSummary(blobRedirectTable?: Map<string, string>): ISummaryTree;

	/**
	 * Propagate the container state when container is attaching or attached.
	 * @param attachState - State of the container.
	 */
	setAttachState(attachState: AttachState.Attaching | AttachState.Attached): void;

	/**
	 * Get pending local state in a serializable format to be given back to a newly loaded container
	 */
	getPendingLocalState(props?: IGetPendingLocalStateProps): unknown;

	/**
	 * Notify runtime that we have processed a saved message, so that it can do async work (applying
	 * stashed ops) after having processed it.
	 */
	notifyOpReplay?(message: ISequencedDocumentMessage): Promise<void>;

	/**
	 * Exposes the entryPoint for the container runtime.
	 * Use this as the primary way of getting access to the user-defined logic within the container runtime.
	 *
	 * @see {@link IContainer.getEntryPoint}
	 */
	getEntryPoint(): Promise<FluidObject>;
}

/**
 * Payload type for IContainerContext.submitBatchFn()
 * @legacy @beta
 */
export interface IBatchMessage {
	contents?: string;
	metadata?: Record<string, unknown>;
	compression?: string;
	referenceSequenceNumber?: number;
}

/**
 * Interface to provide access to snapshots and policies to the Runtime layer. This should follow the Loader / Runtime
 * layer compatibility rules.
 *
 * @remarks It contains a subset of APIs from {@link @fluidframework/driver-definitions#IDocumentStorageService} but
 * allows the Runtime to not support layer compatibility with the Driver layer. Instead, it supports compatibility
 * with the Loader layer which it already does.
 *
 * @legacy @beta
 */
export interface IContainerStorageService {
	/**
	 * Policies implemented/instructed by driver.
	 *
	 * @deprecated This will be removed in a future release. The Runtime layer only needs `maximumCacheDurationMs`
	 * policy which is added as a separate property.
	 */
	readonly policies?: IDocumentStorageServicePolicies | undefined;

	/**
	 * See {@link @fluidframework/driver-definitions#IDocumentStorageServicePolicies.maximumCacheDurationMs}
	 */
	readonly maximumCacheDurationMs?: IDocumentStorageServicePolicies["maximumCacheDurationMs"];

	/**
	 * Returns the snapshot tree.
	 * @param version - Version of the snapshot to be fetched.
	 * @param scenarioName - scenario in which this api is called. This will be recorded by server and would help
	 * in debugging purposes to see why this call was made.
	 */
	// TODO: use `undefined` instead.
	// eslint-disable-next-line @rushstack/no-new-null
	getSnapshotTree(version?: IVersion, scenarioName?: string): Promise<ISnapshotTree | null>;

	/**
	 * Returns the snapshot which can contain other artifacts too like blob contents, ops etc. It is different from
	 * `getSnapshotTree` api in that, that API only returns the snapshot tree from the snapshot.
	 * @param snapshotFetchOptions - Options specified by the caller to specify and want certain behavior from the
	 * driver when fetching the snapshot.
	 */
	getSnapshot?(snapshotFetchOptions?: ISnapshotFetchOptions): Promise<ISnapshot>;

	/**
	 * Retrieves all versions of the document starting at the specified versionId - or null if from the head
	 * @param versionId - Version id of the requested version.
	 * @param count - Number of the versions to be fetched.
	 * @param scenarioName - scenario in which this api is called. This will be recorded by server and would help
	 * in debugging purposes to see why this call was made.
	 * @param fetchSource - Callers can specify the source of the response. For ex. Driver may choose to cache
	 * requests and serve data from cache. That will result in stale info returned. Callers can disable this
	 * functionality by passing fetchSource = noCache and ensuring that driver will return latest information
	 * from storage.
	 */
	getVersions(
		// TODO: use `undefined` instead.
		// eslint-disable-next-line @rushstack/no-new-null
		versionId: string | null,
		count: number,
		scenarioName?: string,
		fetchSource?: FetchSource,
	): Promise<IVersion[]>;

	/**
	 * Creates a blob out of the given buffer
	 */
	createBlob(file: ArrayBufferLike): Promise<ICreateBlobResponse>;

	/**
	 * Reads the object with the given ID, returns content in arrayBufferLike
	 */
	readBlob(id: string): Promise<ArrayBufferLike>;

	/**
	 * Uploads a summary tree to storage using the given context for reference of previous summary handle.
	 * The ISummaryHandles in the uploaded tree should have paths to indicate which summary object they are
	 * referencing from the previously acked summary.
	 * Returns the uploaded summary handle.
	 */
	uploadSummaryWithContext(summary: ISummaryTree, context: ISummaryContext): Promise<string>;
}

/**
 * IContainerContext is fundamentally just the set of things that an IRuntimeFactory (and IRuntime) will consume from the
 * loader layer.
 * It gets passed into the {@link (IRuntimeFactory:interface).instantiateRuntime} call.
 *
 * @privateremarks
 * Only include members on this interface if you intend them to be consumed/called from the runtime layer.
 *
 * TODO: once `@alpha` tag is removed, `unknown` should be removed from submitSignalFn.
 * See {@link https://dev.azure.com/fluidframework/internal/_workitems/edit/7462}
 *
 * @legacy @beta
 */
export interface IContainerContext {
	/**
	 * Not recommended for general use, is used in some cases to control various runtime behaviors.
	 *
	 * @remarks
	 * Used to be ILoaderOptions, this is staging for eventual removal.
	 */
	// eslint-disable-next-line @typescript-eslint/no-explicit-any
	readonly options: Record<string | number, any>;
	readonly clientId: string | undefined;
	readonly clientDetails: IClientDetails;
	readonly storage: IContainerStorageService;
	readonly connected: boolean;
	readonly baseSnapshot: ISnapshotTree | undefined;

	/**
	 * Gets the current connection state of the container.
	 *
	 * @remarks
	 * This provides more detailed connection state information beyond the simple boolean `connected` property.
	 * Available starting from version 2.52.0. Property is not present in older versions.
	 */
	readonly getConnectionState?: () => ConnectionState;
	/**
	 * @deprecated Please use submitBatchFn & submitSummaryFn
	 */
	readonly submitFn: (
		type: MessageType,
		// eslint-disable-next-line @typescript-eslint/no-explicit-any
		contents: any,
		batch: boolean,
		// eslint-disable-next-line @typescript-eslint/no-explicit-any
		appData?: any,
	) => number;
	/**
	 * @returns clientSequenceNumber of last message in a batch
	 */
	readonly submitBatchFn: (batch: IBatchMessage[], referenceSequenceNumber?: number) => number;
	readonly submitSummaryFn: (
		summaryOp: ISummaryContent,
		referenceSequenceNumber?: number,
	) => number;
	readonly submitSignalFn: (contents: unknown, targetClientId?: string) => void;
	readonly disposeFn?: (error?: ICriticalContainerError) => void;
	readonly closeFn: (error?: ICriticalContainerError) => void;
	readonly deltaManager: IDeltaManager<ISequencedDocumentMessage, IDocumentMessage>;
	readonly quorum: IQuorumClients;
	readonly audience: IAudience;
	/**
	 * Signal-based audience provides a view of the audience that only relies
	 * on system signals which will be updated more quickly than
	 * {@link IContainerContext#audience} that relies on ops for write clients.
	 * Being signal-based the write members are inherently less reliable than
	 * {@link IContainerContext#audience}.
	 *
	 * @system
	 */
	readonly signalAudience?: IAudience;
	readonly loader: ILoader;
	// The logger implementation, which would support tagged events, should be provided by the loader.
	readonly taggedLogger: ITelemetryBaseLogger;
	pendingLocalState?: unknown;

	/**
	 * Ambient services provided with the context
	 */
	readonly scope: FluidObject;

	/**
	 * Get an absolute url for a provided container-relative request.
	 * @param relativeUrl - A relative request within the container
	 *
	 * TODO: Optional for backwards compatibility. Make non-optional in version 0.19
	 */
	getAbsoluteUrl?(relativeUrl: string): Promise<string | undefined>;

	/**
	 * Indicates the attachment state of the container to a host service.
	 */
	readonly attachState: AttachState;

	getLoadedFromVersion(): IVersion | undefined;

	updateDirtyContainerState(dirty: boolean): void;

	/**
	 * @deprecated This has been deprecated. It was used internally and there is no replacement.
	 */
	readonly supportedFeatures?: ReadonlyMap<string, unknown>;

	/**
	 * WARNING: this id is meant for telemetry usages ONLY, not recommended for other consumption
	 * This id is not supposed to be exposed anywhere else. It is dependant on usage or drivers
	 * and scenarios which can change in the future.
	 * @deprecated 2.0.0-internal.5.2.0 - The docId is already logged by the {@link IContainerContext.taggedLogger} for
	 * telemetry purposes, so this is generally unnecessary for telemetry.
	 * If the id is needed for other purposes it should be passed to the consumer explicitly.
	 *
	 * @privateremarks Tracking in AB#5714
	 */
	readonly id: string;

	/**
	 * This contains all parts of a snapshot like blobContents, ops etc.
	 */
	readonly snapshotWithContents?: ISnapshot;
}

/**
 * @legacy @beta
 */
export const IRuntimeFactory: keyof IProvideRuntimeFactory = "IRuntimeFactory";

/**
 * @legacy @beta
 */
export interface IProvideRuntimeFactory {
	readonly IRuntimeFactory: IRuntimeFactory;
}

/**
 * Exported module definition
 *
 * Provides the entry point for the ContainerContext to load the proper IRuntime
 * to start up the running instance of the Container.
 * @legacy @beta
 */
export interface IRuntimeFactory extends IProvideRuntimeFactory {
	/**
	 * Instantiates a new IRuntime for the given IContainerContext to proxy to
	 * This is the main entry point to the Container's business logic
	 *
	 * @param context - container context to be supplied to the runtime
	 * @param existing - whether to instantiate for the first time or from an existing context
	 */
	instantiateRuntime(context: IContainerContext, existing: boolean): Promise<IRuntime>;
}

/**
 * Defines list of properties expected for getPendingLocalState
 * @legacy @beta
 */
export interface IGetPendingLocalStateProps {
	/**
	 * Indicates the container will close after getting the pending state. Used internally
	 * to wait for blobs to be attached to a DDS and collect generated ops before closing.
	 */
	readonly notifyImminentClosure: boolean;

	/**
	 * Abort signal to stop waiting for blobs to get attached to a DDS. When triggered,
	 * only blobs attached will be collected in the pending state.
	 * Intended to be used in the very rare scenario in which getLocalPendingState go stale due
	 * to a blob failed to be referenced. Such a blob will be lost but the rest of the state will
	 * be preserved and collected.
	 */
	readonly stopBlobAttachingSignal?: AbortSignal;

	/**
	 * Date to be used as the starting time of a session. This date is updated in case we refresh the
	 * base snapshot since we won't be referencing ops older than the new snapshot.
	 */
	readonly sessionExpiryTimerStarted?: number;

	/**
	 * Snapshot sequence number. It will help the runtime to know which ops should still be stashed.
	 */
	readonly snapshotSequenceNumber?: number;
}
